package com.szh.service.impl;

import com.google.gson.Gson;
import com.szh.config.WxPayConfig;
import com.szh.domain.OrderInfo;
import com.szh.domain.RefundInfo;
import com.szh.enums.OrderStatus;
import com.szh.enums.PayType;
import com.szh.enums.wxpay.WxApiType;
import com.szh.enums.wxpay.WxNotifyType;
import com.szh.enums.wxpay.WxRefundStatus;
import com.szh.enums.wxpay.WxTradeState;
import com.szh.service.OrderInfoService;
import com.szh.service.PaymentInfoService;
import com.szh.service.RefundInfoService;
import com.szh.service.WxPayService;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Service
public class WxPayServiceImpl implements WxPayService {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(WxPayServiceImpl.class);

    private final ReentrantLock lock = new ReentrantLock();

    @Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    private CloseableHttpClient wxPayNoSignClient; //无需应答签名

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private PaymentInfoService paymentInfoService;

    @Resource
    private RefundInfoService refundsInfoService;

    /**
     * 调用统一下单API，生成微信支付二维码
     * @param productId 商品id
     * @return 返回支付二维码链接和订单号
     * @throws Exception Exception
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED,
                   isolation = Isolation.REPEATABLE_READ,
                   rollbackFor = Exception.class)
    public Map<String, Object> nativePay(Long productId) throws Exception {
        LOGGER.info("生成订单");
        OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
        String codeUrl = orderInfo.getCodeUrl();
        if (!StringUtils.isEmpty(codeUrl)) {
            LOGGER.warn("订单 ===> {} 已存在，二维码链接已保存", orderInfo.getOrderNo());
            //直接返回支付二维码链接和订单号
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl", codeUrl);
            map.put("orderNo", orderInfo.getOrderNo());
            return map;
        }

        LOGGER.info("调用统一下单API");
        String nativePayUrl = wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType());
        HttpPost httpPost = new HttpPost(nativePayUrl);
        //请求body参数
        Map paramsMap = new HashMap();
        paramsMap.put("appid", wxPayConfig.getAppid());
        paramsMap.put("mchid", wxPayConfig.getMchId());
        paramsMap.put("description", orderInfo.getTitle());
        paramsMap.put("out_trade_no", orderInfo.getOrderNo());
        String notifyUrl = wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType());
        paramsMap.put("notify_url", notifyUrl);
        Map amountMap = new HashMap();
        amountMap.put("total", orderInfo.getTotalFee());
        amountMap.put("currency", "CHY");
        paramsMap.put("amount", amountMap);
        //将请求body参数转换成json字符串
        Gson gson = new Gson();
        String jsonParams = gson.toJson(paramsMap);
        LOGGER.info("请求参数 ===> {}", jsonParams);
        //将请求参数设置到请求对象中
        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json"); //设置请求传输类型json
        httpPost.setEntity(entity); //设置请求体
        httpPost.setHeader("Accept", "application/json"); //设置请求头信息
        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity()); //响应体
            int statusCode = response.getStatusLine().getStatusCode(); //响应状态码
            if (statusCode == 200) { //处理成功
                LOGGER.info("成功，返回结果 ===> {}", bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                LOGGER.info("成功");
            } else {
                LOGGER.error("Native下单失败，响应码 ===> {}，响应结果 ===> {}", statusCode, bodyAsString);
                throw new IOException("request failed");
            }
            //将响应结果转换成Map
            Map<String, String> map = gson.fromJson(bodyAsString, Map.class);
            codeUrl = map.get("code_url");
            String orderNo = orderInfo.getOrderNo();
            //保存订单支付二维码
            orderInfoService.saveCodeUrl(orderNo, codeUrl);
            //返回支付二维码链接和订单号
            Map<String, Object> resultMap = new HashMap<>();
            resultMap.put("codeUrl", codeUrl);
            resultMap.put("orderNo", orderNo);
            return resultMap;
        } finally {
            response.close();
        }
    }

    /**
     * 处理订单
     * @param bodyMap bodyMap
     * @throws GeneralSecurityException GeneralSecurityException
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED,
                   isolation = Isolation.REPEATABLE_READ,
                   rollbackFor = Exception.class)
    public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
        LOGGER.info("处理订单");
        //解密报文
        String plainText = decryptFromResource(bodyMap);
        //将解密后的明文转换成Map
        Gson gson = new Gson();
        HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String) plainTextMap.get("out_trade_no");
        //在对业务数据进行状态检查和处理之前，要采用数据锁进行并发控制，以避免函数重入造成的数据混乱
        if (lock.tryLock()) {
            try {
                //处理重复的通知，接口调用的幂等性：无论接口被调用多少次，产生的结果是一致的
                //根据订单号查询订单状态，如果不是未支付，则接口直接返回
                String orderStatus = orderInfoService.queryOrderStatus(orderNo);
                if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
                    return;
                }
                //根据订单号更新订单状态，支付成功之后，更新订单状态为支付成功
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
                //记录支付日志
                paymentInfoService.createPaymentInfo(plainText);
            } finally {
                lock.unlock();
            }
        }
    }

    /**
     * 取消订单
     * @param orderNo 订单号
     * @throws Exception Exception
     */
    @Override
    public void cancelOrder(String orderNo) throws Exception {
        //调用微信支付的关闭订单接口
        this.closeOrder(orderNo);
        //更新订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
    }

    /**
     * 查询订单，微信支付的查询订单接口调用
     * @param orderNo 订单号
     * @return String
     * @throws Exception Exception
     */
    @Override
    public String queryOrder(String orderNo) throws Exception {
        LOGGER.info("微信支付的查询订单接口调用 ===> {}", orderNo);
        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");
        //完成签名并执行请求
        try (CloseableHttpResponse response = wxPayClient.execute(httpGet)) {
            String bodyAsString = EntityUtils.toString(response.getEntity()); //响应体
            int statusCode = response.getStatusLine().getStatusCode(); //响应状态码
            if (statusCode == 200) { //处理成功
                LOGGER.info("成功200，返回结果 ===> {}", bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                LOGGER.info("成功204");
            } else {
                LOGGER.error("Native下单失败，响应码 ===> {}，返回结果 ===> {}", statusCode, bodyAsString);
                throw new IOException("request failed");
            }
            return bodyAsString;
        }
    }

    /**
     * 根据订单号查询微信支付查单接口，核实订单状态
     * 如果订单已支付，则更新商户端订单状态，并记录支付日志
     * 如果订单未支付，则调用关单接口关闭订单，并更新商户端订单状态
     * @param orderNo orderNo
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED,
                   isolation = Isolation.REPEATABLE_READ,
                   rollbackFor = Exception.class)
    public void checkOrderStatus(String orderNo) throws Exception {
        LOGGER.warn("根据订单号核实订单状态 ===> {}", orderNo);
        //调用微信支付查单接口
        String result = this.queryOrder(orderNo);
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
        //获取微信支付端的订单状态
        String tradeState = resultMap.get("trade_state");
        //判断订单状态
        if (WxTradeState.SUCCESS.getType().equals(tradeState)) {
            LOGGER.warn("核实订单已支付 ===> {}", orderNo);
            //如果确认订单已支付则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
            //记录支付日志
            paymentInfoService.createPaymentInfo(result);
        }
        if (WxTradeState.NOTPAY.getType().equals(tradeState)) {
            LOGGER.warn("核实订单未支付 ===> {}", orderNo);
            //如果订单未支付，则调用关单接口
            this.closeOrder(orderNo);
            //更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
        }
    }

    /**
     * 申请退款
     * @param orderNo 订单号
     * @param reason 退款原因
     * @throws Exception Exception
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED,
                   isolation = Isolation.REPEATABLE_READ,
                   rollbackFor = Exception.class)
    public void refund(String orderNo, String reason) throws Exception {
        LOGGER.info("创建退款单记录");
        //根据订单号创建退款单
        RefundInfo refundInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);
        LOGGER.info("调用微信支付申请退款接口");
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);
        //组装请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("out_trade_no", orderNo);
        paramsMap.put("out_refund_no", refundInfo.getRefundNo());
        paramsMap.put("reason", reason);
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));
        Map amountMap = new HashMap();
        amountMap.put("refund", refundInfo.getRefund());
        amountMap.put("total", refundInfo.getTotalFee());
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);
        //将请求body参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        LOGGER.info("请求参数 ===> {}", jsonParams);
        //将请求参数设置到请求对象中
        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json"); //设置请求传输类型json
        httpPost.setEntity(entity); //设置请求体
        httpPost.setHeader("Accept", "application/json"); //设置请求头信息
        //完成签名并执行请求
        try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) {
            String bodyAsString = EntityUtils.toString(response.getEntity()); //响应体
            int statusCode = response.getStatusLine().getStatusCode(); //响应状态码
            if (statusCode == 200) { //处理成功
                LOGGER.info("成功，返回结果 ===> {}", bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                LOGGER.info("成功");
            } else {
                LOGGER.error("Native下单失败，响应码 ===> {}，响应结果 ===> {}", statusCode, bodyAsString);
                throw new IOException("request failed");
            }
            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
            //更新退款单
            refundsInfoService.updateRefundForWxPay(bodyAsString);
        }
    }

    /**
     * 查询单笔退款
     * @param refundNo 退款单号
     * @return String
     * @throws Exception Exception
     */
    @Override
    public String queryRefund(String refundNo) throws Exception {
        LOGGER.info("查询退款接口调用 ===> {}", refundNo);
        String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
        url = wxPayConfig.getDomain().concat(url);
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");
        try (CloseableHttpResponse response = wxPayClient.execute(httpGet)) {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                LOGGER.info("成功, 查询退款返回结果 ===> {}" + bodyAsString);
            } else if (statusCode == 204) {
                LOGGER.info("成功");
            } else {
                throw new RuntimeException("查询退款异常, 响应码 = " + statusCode + ", 查询退款返回结果 = " + bodyAsString);
            }
            return bodyAsString;
        }
    }

    /**
     * 根据退款单号核实退款单状态
     * @param refundNo 退款单号
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED,
                   isolation = Isolation.REPEATABLE_READ,
                   rollbackFor = Exception.class)
    public void checkRefundStatus(String refundNo) throws Exception {
        LOGGER.warn("根据退款单号核实退款单状态 ===> {}", refundNo);
        //调用查询退款单接口
        String result = this.queryRefund(refundNo);
        //组装json请求体字符串
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
        //获取微信支付端退款状态
        String status = resultMap.get("status");
        String orderNo = resultMap.get("out_trade_no");
        if (WxRefundStatus.SUCCESS.getType().equals(status)) {
            LOGGER.warn("核实订单已退款成功 ===> {}", refundNo);
            //如果确认退款成功，则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
            //更新退款单
            refundsInfoService.updateRefundForWxPay(result);
        }
        if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
            LOGGER.warn("核实订单退款异常  ===> {}", refundNo);
            //如果确认退款成功，则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
            //更新退款单
            refundsInfoService.updateRefundForWxPay(result);
        }
    }

    /**
     * 处理退款单
     * @param bodyMap bodyMap
     * @throws Exception Exception
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED,
                   isolation = Isolation.REPEATABLE_READ,
                   rollbackFor = Exception.class)
    public void processRefund(Map<String, Object> bodyMap) throws Exception {
        LOGGER.info("处理退款单");
        //解密报文
        String plainText = decryptFromResource(bodyMap);
        //将明文转换成map
        Gson gson = new Gson();
        HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String) plainTextMap.get("out_trade_no");
        if (lock.tryLock()) {
            try {
                String orderStatus = orderInfoService.queryOrderStatus(orderNo);
                //如果订单状态不是退款中，直接返回
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
                    return;
                }
                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
                //更新退款单
                refundsInfoService.updateRefundForWxPay(plainText);
            } finally {
                lock.unlock();
            }
        }
    }

    /**
     * 申请账单，获取账单url
     * @param billDate 账单日期
     * @param type 账单类型
     * @return String
     * @throws Exception Exception
     */
    @Override
    public String queryBill(String billDate, String type) throws Exception {
        LOGGER.warn("申请账单接口调用 ===> {}, {}", billDate, type);
        String url = "";
        if ("tradebill".equals(type)) { //交易账单
            url = WxApiType.TRADE_BILLS.getType();
        } else if ("fundflowbill".equals(type)) { //资金帐单
            url = WxApiType.FUND_FLOW_BILLS.getType();
        } else {
            throw new RuntimeException("不支持的账单类型");
        }
        url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Accept", "application/json");
        try (CloseableHttpResponse response = wxPayClient.execute(httpGet)) {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                LOGGER.info("成功, 申请账单返回结果 ===> {}", bodyAsString);
            } else if (statusCode == 204) {
                LOGGER.info("成功");
            } else {
                throw new RuntimeException("申请账单异常, 响应码 = " + statusCode + ", 申请账单返回结果 = " + bodyAsString);
            }
            //获取账单下载地址
            Gson gson = new Gson();
            Map<String, String> resultMap = gson.fromJson(bodyAsString, Map.class);
            return resultMap.get("download_url");
        }
    }

    /**
     * 下载账单
     * @param billDate 账单日期
     * @param type 账单类型
     * @return String
     * @throws Exception Exception
     */
    @Override
    public String downloadBill(String billDate, String type) throws Exception {
        LOGGER.warn("下载账单接口调用 ===> {}, ===> {}", billDate, type);
        //获取账单下载地址
        String downloadUrl = this.queryBill(billDate, type);
        HttpGet httpGet = new HttpGet(downloadUrl);
        httpGet.addHeader("Accept", "application/json");
        try (CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet)) {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                LOGGER.info("成功, 下载账单返回结果 ===> {}", bodyAsString);
            } else if (statusCode == 204) {
                LOGGER.info("成功");
            } else {
                throw new RuntimeException("下载账单异常, 响应码 = " + statusCode + ", 下载账单返回结果 = " + bodyAsString);
            }
            return bodyAsString;
        }
    }

    /**
     * 微信支付的关闭订单接口
     * @param orderNo 订单号
     */
    private void closeOrder(String orderNo) throws Exception {
        LOGGER.info("微信支付的关闭订单接口调用，订单号 ===> {}", orderNo);
        //创建远程请求对象
        String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url);
        HttpPost httpPost = new HttpPost(url);
        //组装json请求体
        Gson gson = new Gson();
        Map<String, String> paramsMap = new HashMap<>();
        paramsMap.put("mchid", wxPayConfig.getMchId());
        String jsonParams = gson.toJson(paramsMap);
        LOGGER.info("请求参数 ===> {}", jsonParams);
        //将请求参数设置到请求对象中
        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json"); //设置请求传输类型json
        httpPost.setEntity(entity); //设置请求体
        httpPost.setHeader("Accept", "application/json"); //设置请求头信息
        //完成签名并执行请求
        try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) {
            int statusCode = response.getStatusLine().getStatusCode(); //响应状态码
            if (statusCode == 200) { //处理成功
                LOGGER.info("成功200");
            } else if (statusCode == 204) { //处理成功，无返回Body
                LOGGER.info("成功204");
            } else {
                LOGGER.error("Native下单失败，响应码 ===> {}", statusCode);
                throw new IOException("request failed");
            }
        }
    }

    /**
     * 对称解密
     * @param bodyMap bodyMap
     * @return plainText
     */
    private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
        LOGGER.info("密文解密");
        //通知数据
        Map<String, String> resourceMap = (Map) bodyMap.get("resource");
        //数据密文
        String ciphertext = resourceMap.get("ciphertext");
        //随机串
        String nonce = resourceMap.get("nonce");
        //附加数据
        String associatedData = resourceMap.get("associated_data");
        LOGGER.info("密文 ===> {}", ciphertext);
        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);
        LOGGER.info("明文 ===> {}", plainText);
        return plainText;
    }
}
